iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0
自我挑戰組

Terraform 繁體中文系列 第 6

Day6-【入門教程】Terraform基礎概念—狀態管理

  • 分享至 

  • xImage
  •  

原簡體中文教程連結: Introduction.《Terraform入門教程》


1.3.2.1. Terraform基礎概念——狀態管理

我們在第一章的末尾提過,當我們成功地執行了一次 terraform apply,建立了期望的基礎設施以後,我們如果再次執行 terraform apply,生成的新的執行計劃將不會包含任何變更,Terraform 會記住當前基礎設施的狀態,並將之與程式碼所描述的期望狀態進行比對。第二次 apply 時,因為當前狀態已經與程式碼描述的狀態一致了,所以會生成一個空的執行計劃。

1.3.2.1.1. 初探狀態文件

在這裡,Terraform 引入了一個獨特的概念——狀態管理,這是 Ansible 等配置管理工具或是自研工具調用 SDK 操作基礎設施的方案所沒有的。簡單來說,Terraform 將每次執行基礎設施變更操作時的狀態訊息保存在一個狀態文件中,預設情況下會保存在當前工作目錄下的文件裡 terraform.tfstate 。例如我們在程式碼中宣告一個 data 和一個resource:

data "ucloud_images" "default" {
  availability_zone = "cn-sh2-01"
  name_regex        = "^CentOS 6.5 64"
  image_type        = "base"
}

resource "ucloud_vpc" "vpc" {
  cidr_blocks = ["10.0.0.0/16"]
  name = "my-vpc"
}

使用 terraform apply 後,我們可以看到 terraform.tfstate 的內容:

{
  "version": 4,
  "terraform_version": "0.13.5",
  "serial": 54,
  "lineage": "a0d89a84-ae5b-8e14-d61b-2d9885e3359a",
  "outputs": {},
  "resources": [
    {
      "mode": "data",
      "type": "ucloud_images",
      "name": "default",
      "provider": "provider[\"registry.terraform.io/ucloud/ucloud\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "availability_zone": "cn-sh2-01",
            "id": "1693951353",
            "ids": [
              "uimage-xiucsl"
            ],
            "image_id": null,
            "image_type": "base",
            "images": [
              {
                "availability_zone": "cn-sh2-01",
                "create_time": "2020-01-09T11:30:34+08:00",
                "description": "",
                "features": [
                  "NetEnhanced",
                  "CloudInit"
                ],
                "id": "uimage-xiucsl",
                "name": "CentOS 6.5 64位",
                "os_name": "CentOS 6.5 64位",
                "os_type": "linux",
                "size": 20,
                "status": "Available",
                "type": "base"
              }
            ],
            "most_recent": false,
            "name_regex": "^CentOS 6.5 64",
            "os_type": null,
            "output_file": null,
            "total_count": 1
          }
        }
      ]
    },
    {
      "mode": "managed",
      "type": "ucloud_vpc",
      "name": "vpc",
      "provider": "provider[\"registry.terraform.io/ucloud/ucloud\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "cidr_blocks": [
              "10.0.0.0/16"
            ],
            "create_time": "2020-11-16T17:00:40+08:00",
            "id": "uvnet-lu2vcdds",
            "name": "my-vpc",
            "network_info": [
              {
                "cidr_block": "10.0.0.0/16"
              }
            ],
            "remark": null,
            "tag": "Default",
            "update_time": "2020-11-16T17:00:40+08:00"
          },
          "private": "bnVsbA=="
        }
      ]
    }
  ]
}

我們可以看到,查詢到的 data 以及建立的 resource 訊息都被以 json 格式保存在 tfstate 文件裡。

我們前面已經說過,由於 tfstate 文件的存在,我們在 terraform apply 之後立即再次 apply 是不會執行任何變更的,那麼如果我們刪除了這個 tfstate 文件,然後再執行 apply 會發生什麼呢?Terraform 讀取不到 tfstate 文件,會認為這是我們第一次建立這組資源,所以它會再一次建立程式碼中描述的所有資源。更加麻煩的是,由於我們前一次建立的資源所對應的狀態訊息被我們刪除了,所以我們再也無法通過執行 terraform destroy 來銷毀和回收這些資源,實際上產生了資源洩漏。所以妥善保存這個狀態文件是非常重要的。

另外,如果我們對 Terraform 的程式碼進行了一些修改,導致產生的執行計劃將會改變狀態,那麼在實際執行變更之前,Terraform 會複製一份當前的 tfstate 文件到同路徑下的 terraform.tfstate.backup 中,以防止由於各種意外導致的 tfstate 損毀。

在 Terraform 發展的極早期,HashiCorp 曾經嘗試過無狀態文件的方案,也就是在執行 Terraform 變更計劃時,給所有涉及到的資源都打上特定的 tag,在下次執行變更時,先通過 tag 讀取相關資源來重建狀態訊息。但因為並不是所有資源都支持打 tag,也不是所有公有雲都支持多 tag,所以 Terraform 最終決定用狀態文件方案。

還有一點,HashiCorp 官方從未公開過 tfstate 的格式,也就是說,HashiCorp 保留隨時修改 tfstate 格式的權力。所以不要試圖手動或是用自研程式碼去修改 tfstate,Terraform 命令行工具提供了相關的指令(我們後續會介紹到),請確保只通過命令行的指令操作狀態文件。

1.3.2.1.2. 極其重要的安全警示——tfstate 是明文的

關於 Terraform 狀態,還有極其重要的事,所有考慮在生產環境使用 Terraform 的人都必須格外小心並再三警惕:Terraform 的狀態文件是明文的,這就意味著程式碼中所使用的一切機密訊息都將以明文的形式保存在狀態文件裡。例如我們回到創建UCloud主機的例子:

data "ucloud_security_groups" "default" {
  type = "recommend_web"
}

data "ucloud_images" "default" {
  availability_zone = "cn-sh2-02"
  name_regex        = "^CentOS 6.5 64"
  image_type        = "base"
}

resource "ucloud_instance" "normal" {
  availability_zone = "cn-sh2-02"
  image_id          = data.ucloud_images.default.images[0].id
  instance_type     = "n-basic-2"
  root_password     = "supersecret1234"
  name              = "tf-example-normal-instance"
  tag               = "tf-example"
  boot_disk_type    = "cloud_ssd"
  security_group = data.ucloud_security_groups.default.security_groups[0].id
  delete_disks_with_instance = true
}

我們在程式碼中明文傳入了 root_password 的值是 supersecret1234,執行了 terraform apply 後我們觀察 tfstate 文件中相關段落:

{
      "mode": "managed",
      "type": "ucloud_instance",
      "name": "normal",
      "provider": "provider[\"registry.terraform.io/ucloud/ucloud\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "allow_stopping_for_update": null,
            "auto_renew": false,
            "availability_zone": "cn-sh2-02",
            "boot_disk_size": 20,
            "boot_disk_type": "cloud_ssd",
            "charge_type": null,
            "cpu": 2,
            "cpu_platform": "Intel/Broadwell",
            "create_time": "2020-11-16T18:06:32+08:00",
            "data_disk_size": null,
            "data_disk_type": null,
            "data_disks": [],
            "delete_disks_with_instance": true,
            "disk_set": [
              {
                "id": "bsi-krv0ilrc",
                "is_boot": true,
                "size": 20,
                "type": "cloud_ssd"
              }
            ],
            "duration": null,
            "expire_time": "1970-01-01T08:00:00+08:00",
            "id": "uhost-u2byoz4i",
            "image_id": "uimage-ku3uri",
            "instance_type": "n-basic-2",
            "ip_set": [
              {
                "internet_type": "Private",
                "ip": "10.25.94.58"
              }
            ],
            "isolation_group": "",
            "memory": 4,
            "min_cpu_platform": null,
            "name": "tf-example-normal-instance",
            "private_ip": "10.25.94.58",
            "remark": "",
            "root_password": "supersecret1234",
            "security_group": "firewall-a0lqq3r3",
            "status": "Running",
            "subnet_id": "subnet-0czucaf2",
            "tag": "tf-example",
            "timeouts": null,
            "user_data": null,
            "vpc_id": "uvnet-0noi3kun"
          },
          "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxODAwMDAwMDAwMDAwLCJkZWxldGUiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=",
          "dependencies": [
            "data.ucloud_images.default",
            "data.ucloud_security_groups.default"
          ]
        }
      ]
    }

可以看到 root_password 的值 supersecret1234 是以明文形式被寫在 tfstate 文件裡的。這是 Terraform 從設計之初就確定的,並且在可見的未來不會有改善。不論你是在程式碼中明文硬編碼,還是使用參數(variable,我們之後的章節會介紹),亦或是妙想天開地使用函數在運行時從外界讀取,都無法改變這個結果。

解決之道有兩種,一種是使用 Vault 或是 AWS Secret Manager 這樣的動態機密管理工俱生成臨時有效的動態機密(比如有效期只有 5 分鐘,即使被他人讀取到,機密也早已失效);另一種就是我們下面將要介紹的——Terraform Backend。

1.3.2.1.3. 生產環境的 tfstate 管理方案——Backend

到目前為止我們的 tfstate 文件是保存在當前工作目錄下的本地文件,假設我們的計算機損壞了,導致文件丟失,那麼 tfstate 文件所對應的資源都將無法管理,而產生資源洩漏。

另外如果我們是一個團隊在使用 Terraform 管理一組資源,團隊成員之間要如何共享這個狀態文件?能不能把 tfstate 文件簽入源程式碼管理工具進行保存?

把 tfstate 文件簽入程式碼管理工具是非常錯誤的,這就好比把資料庫簽入了源程式碼管理工具,如果兩個人同時簽出了同一份 tfstate,並且對程式碼做了不同的修改,又同時 apply 了,這時想要把 tfstate 簽入源碼管理系統可能會遭遇到無法解決的衝突。

為了解決狀態文件的存儲和共享問題,Terraform 引入了遠端狀態存儲機制,也就是 Backend。Backend 是一種抽象的遠程存儲接口,如同 Provider 一樣,Backend 也支持多種不同的遠程存儲服務:

支持的Backend列表(左側)
圖1.3.2/1 - 支持的Backend列表(左側)

Terraform Remote Backend 分為兩種:

  • 標準:支持遠端狀態存儲與狀態鎖
  • 增強:在標準的基礎上支持遠端操作(在遠端服務器上執行 plan、apply 等操作)
    目前增強型 Backend 只有 Terraform Cloud 雲服務一種。

狀態鎖是指,當針對一個 tfstate 進行變更操作時,可以針對該狀態文件添加一把全局鎖,確保同一時間只能有一個變更被執行。不同的 Backend 對狀態鎖的支持不盡相同,實現狀態鎖的機制也不盡相同,例如 consul backend 就通過一個 .lock 節點來充當鎖,一個 .lockinfo 節點來描述鎖對應的會話訊息,tfstate 文件被保存在 backend 定義的路徑節點內;s3 backend 則需要用戶傳入一個 Dynamodb 表來存放鎖訊息,而 tfstate 文件被存儲在 s3 存儲桶裡。名為 etcd 的 backend 對應的是 etcd v2,它不支持狀態鎖;etcdv3 則提供了對狀態鎖的支持,等等等等。讀者可以根據實際情況,挑選自己合適的 Backend。接下來我將以 consul 為範例為讀者演示 Backend 機制。

1.3.2.1.4. Consul簡介以及安裝

Consul 是 HashiCorp 推出的一個開源工具,主要用來解決服務發現、配置中心以及 Service Mesh 等問題;Consul 本身也提供了類似 ZooKeeper、Etcd 這樣的分佈式鍵值存儲服務,具有基於 Gossip 協議的最終一致性,所以可以被用來充當 Terraform Backend 存儲。

安裝 Consul 十分簡單,如果你是 Ubuntu 用戶:

curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install -y consul

對於 CentOS 用戶:

sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install consul

對於 MacOS 用戶:

brew tap hashicorp/tap
brew install hashicorp/tap/consul

對於 Windows 用戶,如果按照前文安裝 Terraform 教程已經配置了 Chocolatey 的話:

choco install consul

安裝完成後的驗證:

$ consul
Usage: consul [--version] [--help] <command> [<args>]

Available commands are:
    acl            Interact with Consul's ACLs
    agent          Runs a Consul agent
    catalog        Interact with the catalog
    config         Interact with Consul's Centralized Configurations
    connect        Interact with Consul Connect
    debug          Records a debugging archive for operators
    event          Fire a new event
    exec           Executes a command on Consul nodes
    force-leave    Forces a member of the cluster to enter the "left" state
    info           Provides debugging information for operators.
    intention      Interact with Connect service intentions
    join           Tell Consul agent to join cluster
    keygen         Generates a new encryption key
    keyring        Manages gossip layer encryption keys
    kv             Interact with the key-value store
    leave          Gracefully leaves the Consul cluster and shuts down
    lock           Execute a command holding a lock
    login          Login to Consul using an auth method
    logout         Destroy a Consul token created with login
    maint          Controls node or service maintenance mode
    members        Lists the members of a Consul cluster
    monitor        Stream logs from a Consul agent
    operator       Provides cluster-level tools for Consul operators
    reload         Triggers the agent to reload configuration files
    rtt            Estimates network round trip time between nodes
    services       Interact with services
    snapshot       Saves, restores and inspects snapshots of Consul server state
    tls            Builtin helpers for creating CAs and certificates
    validate       Validate config files/directories
    version        Prints the Consul version
    watch          Watch for changes in Consul

安裝完 Consul 後,我們可以啟動一個測試版 Consul 服務:

$ consul agent -dev

Consul 會在本機 8500 端口開放 Http 終結點,我們可以通過瀏覽器訪問 http://localhost:8500

Consul 的 GUI 界面
圖1.3.2/2 - Consul的GUI界面

1.3.2.1.5. 使用Backend

我們寫一個可以免費執行的簡單 Terraform 程式碼:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }
  backend "consul" {
    address = "localhost:8500"
    scheme  = "http"
    path    = "my-ucloud-project"
  }
}

provider "ucloud" {
  public_key  = "JInqRnkSY8eAmxKFRxW9kVANYThfIW9g2diBbZ8R8"
  private_key = "8V5RClzreyKBxrJ2GsePjfDYHy55yYsIIy3Qqzjjah0C0LLxhXkKSzEKFWkATqu4U"
  project_id  = "org-a2pbab"
  region      = "cn-sh2"
}

resource "ucloud_vpc" "vpc" {
  cidr_blocks = ["10.0.0.0/16"]
}

注意要把程式碼中的 public_keyprivate_keyproject_id 換成你自己的。

在 terraform 節中,我們添加了 backend 配置節,指定使用 localhost:8500 為地址(也就是我們剛才啟動的測試版 Consul 服務),指定使用 http 協議訪問該地址,指定 tfstate 文件存放在 Consul 鍵值存儲服務的路徑 my-ucloud-project 下。

當我們執行完 terraform apply 後,我們訪問 http://localhost:8500/ui/dc1/kv

Consul 中可以看到名為 my-ucloud-project 的鍵
圖1.3.2/3 - Consul 中可以看到名為 my-ucloud-project 的鍵
可以看到 my-ucloud-project,點擊進入:

鍵的內容
圖1.3.2/4 - 鍵的內容
可以看到,原本保存在工作目錄下的 tfstate 文件的內容,被保存在了 Consul 的名為 my-ucloud-project 的鍵下。

讓我們執行 terraform destroy 後,重新訪問 http://localhost:8500/ui/dc1/kv

鍵依然存在
圖1.3.2/5 - 鍵依然存在
可以看到,my-ucloud-project 這個鍵仍然存在。讓我們點擊進去:

內容已被清空
圖1.3.2/6 - 內容已被清空
可以看到,它的內容為空,代表基礎設施已經被成功銷毀。

1.3.2.1.6. 觀察鎖文件

那麼在這個過程裡,鎖究竟在哪裡?我們如何能夠體驗到鎖的存在?讓我們對程式碼進行一點修改:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }
  backend "consul" {
    address = "localhost:8500"
    scheme  = "http"
    path    = "my-ucloud-project"
  }
}

provider "ucloud" {
  public_key  = "JInqRnkSY8eAmxKFRxW9kVANYThfIW9g2diBbZ8R8"
  private_key = "8V5RClzreyKBxrJ2GsePjfDYHy55yYsIIy3Qqzjjah0C0LLxhXkKSzEKFWkATqu4U"
  project_id  = "org-a2pbab"
  region      = "cn-sh2"
}

resource "ucloud_vpc" "vpc" {
  cidr_blocks = ["10.0.0.0/16"]
  provisioner "local-exec" {
    command = "sleep 1000"
  }
}

這次的變化是我們在 ucloud_vpc 的定義上添加了一個 local-exec 類型的 provisioner。provisioner 我們在後續的章節中會專門敘述,在這裡讀者只需要理解,Terraform 進程在成功創建了該 VPC 後,會在執行 Terraform 命令行的機器上執行一條命令:sleep 1000,這個時間足以將 Terraform 進程阻塞足夠長的時間,以便讓我們觀察鎖訊息了。

讓我們執行 terraform apply,這一次 apply 將會被 sleep 阻塞,而不會成功完成:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # ucloud_vpc.vpc will be created
  + resource "ucloud_vpc" "vpc" {
      + cidr_blocks  = [
          + "10.0.0.0/16",
        ]
      + create_time  = (known after apply)
      + id           = (known after apply)
      + name         = (known after apply)
      + network_info = (known after apply)
      + remark       = (known after apply)
      + tag          = "Default"
      + update_time  = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

ucloud_vpc.vpc: Creating...
ucloud_vpc.vpc: Provisioning with 'local-exec'...
ucloud_vpc.vpc (local-exec): Executing: ["/bin/sh" "-c" "sleep 1000"]
ucloud_vpc.vpc: Still creating... [10s elapsed]
...

讓我們重新訪問 http://localhost:8500/ui/dc1/kv

多了一個同名的文件夾
圖1.3.2/7 - 多了一個同名的文件夾
這一次情況發生了變化,我們看到除了 my-ucloud-project 這個鍵之外,還多了一個同名的文件夾。讓我們點擊進入文件夾:

my-ucloud-project文件夾內部
圖1.3.2/8 - my-ucloud-project文件夾內部
在這裡我們成功觀測到了 .lock 和 .lockinfo 文件。讓我們點擊 .lock 看看:

.lock內容
圖1.3.2/9 - .lock內容
Consul UI 提醒我們,該鍵值對目前正被鎖定,而它的內容是空。讓我們查看 .lockinfo 的內容:

.lockinfo內容
圖1.3.2/10 - .lockinfo內容
.lockinfo 裡記錄了鎖 ID、我們執行的操作,以及其他的一些訊息。

讓我們另起一個新的命令行窗口,在同一個工作目錄下嘗試另一次執行 terraform apply

$ terraform apply
Acquiring state lock. This may take a few moments...

Error: Error locking state: Error acquiring the state lock: Lock Info:
  ID:        563ef038-610e-85cf-ca89-9e3b4a830b67
  Path:      my-ucloud-project
  Operation: OperationTypeApply
  Who:       byers@ByersMacBook-Pro.local
  Version:   0.13.5
  Created:   2020-11-16 11:53:50.473561 +0000 UTC
  Info:      consul session: 9bd80a12-bc2f-1c5b-af0f-cdb07e5e69dc


Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.

可以看到,同時另一個人試圖對同一個 tfstate 執行變更的嘗試失敗了,因為它無法順利獲取到鎖。

讓我們用 ctrl-c 終止原先被阻塞的的 terraform apply 執行,然後重新訪問 http://localhost:8500/ui/dc1/kv

重新訪問 Consul
圖1.3.2/11 - 重新訪問 Consul
可以看到,包含鎖的文件夾消失了。Terraform 命令行執行序在接收到 ctrl-c 信號時,會首先把當前已知的狀態訊息寫入 Backend 內,然後釋放 Backend 上的鎖,再結束進程。但是如果 Terraform 執行序是被強行殺死,或是機器掉電,那麼在 Backend 上就會遺留一個鎖,導致後續的操作都無法執行,這時我們需要用命令 terraform force-unlock 強行刪除鎖,我們將在後續的章節中詳細敘述。

1.3.2.1.7. 小貼士——假如一開始 Backend 配置寫錯了會怎麼樣

讓我們假設我們擁有一個乾淨的工作目錄,我們新建了一個 main.tf 程式碼文件,在 terraform 配置節當中配置瞭如下 backend:

backend "consul" {
    address = "localhost:8600"
    scheme  = "http"
    path    = "my-ucloud-project"
}

我們把 address 參數寫錯了,端口號從 8500 寫成了 8600,這是我們執行一次 terraform init

$ terraform init

Initializing the backend...

Successfully configured the backend "consul"! Terraform will automatically
use this backend unless the backend configuration changes.

Error: Failed to get existing workspaces: Get "http://localhost:8600/v1/kv/my-ucloud-project-env:?keys=&separator=%2F": EOF

並不奇怪,Terraform 抱怨無法連接到 localhost:8600。這時我們把 backend 配置的端口糾正回 8500,重新執行 init 看看:

$ terraform init

Initializing the backend...
Backend configuration changed!

Terraform has detected that the configuration specified for the backend
has changed. Terraform will now check for existing state in the backends.



Error: Error inspecting states in the "consul" backend:
    Get "http://localhost:8600/v1/kv/my-ucloud-project-env:?keys=&separator=%2F": EOF

Prior to changing backends, Terraform inspects the source and destination
states to determine what kind of migration steps need to be taken, if any.
Terraform failed to load the states. The data in both the source and the
destination remain unmodified. Please resolve the above error and try again.

還是錯誤,Terraform 還是試圖連接 localhost:8600,並且這次的報錯訊息提示我們需要幫助它解決錯誤,以便它能夠決定如何進行狀態數據的遷移。

這是因為 Terraform 發現 Backend 的配置發生了變化,所以它嘗試從原先的 Backend 讀取狀態數據,並且嘗試將之遷移到新的 Backend,但因為原先的 Backend 是錯的,所以它會再次抱怨連接不上 localhost:8500

.terraform 目錄下多了一個 terraform.tfstate 文件
圖1.3.2/12 - .terraform 目錄下多了一個 terraform.tfstate 文件
如果我們檢查此時的工作目錄下的 .terraform 目錄,會看到其中多了一個本地的 terraform.tfstate。檢查它的內容:

{
    "version": 3,
    "serial": 2,
    "lineage": "aa296584-3606-f9b0-78da-7c5563b46c7b",
    "backend": {
        "type": "consul",
        "config": {
            "access_token": null,
            "address": "localhost:8600",
            "ca_file": null,
            "cert_file": null,
            "datacenter": null,
            "gzip": null,
            "http_auth": null,
            "key_file": null,
            "lock": null,
            "path": "my-ucloud-project",
            "scheme": "http"
        },
        "hash": 3939494596
    },
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {},
            "depends_on": []
        }
    ]
}

可以看到它把最初的 Backend 配置記錄在了裡面,地址仍然是 localhost:8600,這就導致了我們即使修正了 Backend 配置,也無法成功 init。在這個場景下,解決方法也很簡單,直接刪除這個本地 tfstate 文件即可。

這個小問題引出了我們的下一個話題——狀態遷移。

1.3.2.1.8. 狀態遷移

讓我們先重啟一下測試版 Consul 服務,清除舊有的狀態。假如我們一開始沒有宣告 backend:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }
}

provider "ucloud" {
  public_key  = "JInqRnkSY8eAmxKFRxW9kVANYThfIW9g2diBbZ8R8"
  private_key = "8V5RClzreyKBxrJ2GsePjfDYHy55yYsIIy3Qqzjjah0C0LLxhXkKSzEKFWkATqu4U"
  project_id  = "org-a2pbab"
  region      = "cn-sh2"
}

resource "ucloud_vpc" "vpc" {
  cidr_blocks = ["10.0.0.0/16"]
}

然後我們執行 terraform init,繼而執行 terraform apply,那麼我們將成功創建雲端資源,並且在工作目錄下會有一個 terraform.tfstate 文件:

{
  "version": 4,
  "terraform_version": "0.13.5",
  "serial": 1,
  "lineage": "a0335546-0039-cccc-467b-5dc3050c8212",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "ucloud_vpc",
      "name": "vpc",
      "provider": "provider[\"registry.terraform.io/ucloud/ucloud\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "cidr_blocks": [
              "10.0.0.0/16"
            ],
            "create_time": "2020-11-16T22:24:38+08:00",
            "id": "uvnet-ssgiofxv",
            "name": "tf-vpc-20201116142437539000000001",
            "network_info": [
              {
                "cidr_block": "10.0.0.0/16"
              }
            ],
            "remark": null,
            "tag": "Default",
            "update_time": "2020-11-16T22:24:38+08:00"
          },
          "private": "bnVsbA=="
        }
      ]
    }
  ]
}

隨後我們加上了之前寫過的指向本機測試 Consul 服務的 backend 宣告,然後執行 terraform init

$ terraform init

Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "consul" backend. No existing state was found in the newly
  configured "consul" backend. Do you want to copy this state to the new "consul"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes

Terraform 成功地檢測到 backend 類型從 local 變為了 consul,並且確認了 Consul 裡同名路徑下沒有狀態文件存在,於是 Terraform 可以替我們把本機的狀態文件遷移到新的 Backend 裡,但這需要我們手工確認。輸入 yes 並且按下 Enter:

Enter a value: yes


Successfully configured the backend "consul"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Using previously-installed ucloud/ucloud v1.22.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

這時讓我們訪問 http://localhost:8500/ui/dc1/kv/my-ucloud-project/edit

查看 my-ucloud-project 的內容
圖1.3.2/13 - 查看 my-ucloud-project 的內容
本機的狀態數據被成功地遷移到了 Consul 裡(雖然和本機的文件並不完全相同,但狀態數據是相同的)。

那假如試圖遷移狀態時,新 backend 的目標路徑上已經存在其他 tfstate 會發生什麼呢?我們簡單地說一下結果,就是 Terraform 會把我們的 tfstate 和新 backend 上既有的其他 tfstate 下載到本機的一個臨時目錄下,然後要求我們人工核對以後決定是否覆蓋既有的 tfstate。

1.3.2.1.9. Backend 配置的動態賦值

有些讀者會注意到,到目前為止我所寫的程式碼裡的配置項基本都是硬編碼的,Terraform 是否支持運行時用變量動態賦值?答案是支持的,Terraform 可以通過 variable 變量來傳值給 provider、data 和 resource。

但有一個例外,那就是 backend 配置。backend 配置只允許硬編碼,或者不傳值。

這個問題是因為 Terraform 運行時本身設計的運行順序導致的,一直到 2019 年 05 月官方才給出了解決方案,那就是“部分配置“ (partial configuration)。

簡單來說就是我們可以在 tf 程式碼的 backend 宣告中不給出具體的配置:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }
  backend "consul" {

  }
}

而在另一個獨立的文件中給出相關配置,例如我們在工作目錄下創建一個名為 backend.hcl 的文件:

address = "localhost:8500"
scheme  = "http"
path    = "my-ucloud-project"

本質上我們就是把原本屬於 backend consul 節的屬性賦值程式碼搬遷到一個獨立的 hcl 文件內,然後我們執行 terraform init 時附加 backend-config 參數:

$ terraform init -backend-config=backend.hcl

這樣也可以初始化成功。通過這種打補丁的方式,我們可以復用他人預先寫好的 Terraform 程式碼,在執行時把屬於我們自己的 Backend 配置訊息以獨立的 backend-config 文件的形式傳入來進行初始化。

1.3.2.1.10. Backend 的權限控制以及版本控制

Backend 本身並沒有設計任何的權限以及版本控制,這方面完全依賴於具體的 Backend 實現。以 AWS S3 為例,我們可以針對不同的 Bucket 設置不同的 IAM,用以防止開發測試人員直接操作生產環境,或是給予部分人員對狀態訊息的只讀權限;另外我們也可以開啟 S3 的版本控制功能,以防我們錯誤修改了狀態文件(Terraform 命令行有修改狀態的相關指令)。

1.3.2.1.11. 狀態的隔離存儲

我們講完 Backend,現在要討論另一個問題。假設我們的 Terraform 程式碼可以創建一個通用的基礎設施,比如說是雲端的一個 eks、aks 集群,或者是一個基於 S3 的靜態網站,那麼我們可能要為很多團隊創建並維護這些相似但要彼此隔離的 Stack,又或者我們要為部署的應用維護開發、測試、預發布、生產四套不同的部署。那麼該如何做到不同的部署,彼此狀態文件隔離存儲和管理呢?

一種簡單的方法就是分成不同的文件夾存儲。

將程式碼複製到不同的文件夾中保存
圖1.3.2/14 - 將程式碼複製到不同的文件夾中保存
我們可以把不同產品不同部門使用的基礎設施分成不同的文件夾,在文件夾內維護相同的程式碼文件,配置不同的 backend-config,把狀態文件保存到不同的 Backend上。這種方法可以給予最大程度的隔離,缺點是我們需要拷貝許多份相同的程式碼。

第二種更加輕量級的方法就是 Workspace。注意,Terraform 開源版的 Workspace 與 Terraform Cloud 雲服務的 Workspace 實際上是兩個不同的概念,我們這裡介紹的是開源版的 Workspace。

Workspace 允許我們在同一個文件夾內,使用同樣的 Backend 配置,但可以維護任意多個彼此隔離的狀態文件。還是我們剛才那個使用測試 Consul 服務作為 Backend 的例子:

重新訪問Consul,目前有一個鍵
圖1.3.2/15 - 重新訪問Consul,目前有一個鍵
當前我們有一個狀態文件,名字是 my-ucloud-project。然後我們在工作目錄下執行這樣的命令:

$ terraform workspace new feature1
Created and switched to workspace "feature1"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

通過調用 workspace 命令,我們成功創建了名為 feature1 的 Workspace。這時我們觀察 .terraform 文件夾:

.terraform
├── environment
├── modules
│   └── modules.json
└── plugins
    ├── registry.terraform.io
    │   ├── ucloud
......

我們會發現多了一個 environment 文件,它的內容是 feature1。這實際上就是 Terraform 用來保存當前上下文環境使用的是哪個 Workspace 的文件。

Consul 中多了一個 my-ucloud-project-env:feature1
圖1.3.2/16 - Consul中多了一個my-ucloud-project-env:feature1
重新觀察 Consul 存儲會發現多了一個文件:my-ucloud-project-env:feature1。這就是 Terraform 為 feature1 這個 Workspace 創建的獨立的狀態文件。讓我們執行一下 apply,然後再看這個文件的內容:

此時my-ucloud-project-env:feature1的內容
圖1.3.2/17 - 此時 my-ucloud-project-env:feature1 的內容
可以看到,狀態被成功寫入了 feature1 的狀態文件。

我們可以通過以下命令來查詢當前 Backend 下所有的 Workspace:

$ terraform workspace list
  default
* feature1

我們有 default 和 feature1 兩個 Workspace,當前我們工作在 feature1 上。我們可以用以下命令切換回default:

$ terraform workspace select default
Switched to workspace "default".

我們可以用以下命令確認我們成功切換回了 default:

$ terraform workspace show
default

我們可以用以下命令刪除 feature1:

$ terraform workspace delete feature1
Deleted workspace "feature1"!

再觀察 Consul 存儲,就會發現 feature1 的狀態文件被刪除了:

my-ucloud-project-env:feature1 被刪除了
圖1.3.2/18 - my-ucloud-project-env:feature1被刪除了
目前支持多工作區的 Backend 有:

  • AzureRM
  • Consul
  • COS
  • GCS
  • Kubernetes
  • Local
  • Manta
  • Postgres
  • Remote
  • S3

1.3.2.1.12. 該使用哪種隔離

相比起多文件夾隔離的方式來說,基於 Workspace 的隔離更加簡單,只需要保存一份程式碼,在程式碼中不需要為 Workspace 編寫額外程式碼,用命令行就可以在不同工作區之間來回切換。

但是 Workspace 的缺點也同樣明顯,由於所有工作區的 Backend 配置是一樣的,所以有權讀寫某一個 Workspace 的人可以讀取同一個 Backend 路徑下所有其他 Workspace;另外 Workspace 是隱式配置的(調用命令行),所以有時人們會忘記自己工作在哪個 Workspace 下。

Terraform 官方為 Workspace 設計的場景是:有時開發人員想要對既有的基礎設施做一些變更,並進行一些測試,但又不想直接冒險修改既有的環境。這時他可以利用 Workspace 複製出一個與既有環境完全一致的平行環境,在這個平行環境裡做一些變更,並進行測試和實驗工作。

Workspace 對應的源程式碼管理模型裡的主幹——分支模型,如果團隊希望維護的是不同產品之間不同的基礎設施,或是開發、測試、預發布、生產環境,那麼最好還是使用不同的文件夾以及不同的 backend-config 進行管理。


原簡體中文教程連結: Introduction.《Terraform入門教程》


上一篇
Day5-【入門教程】Terraform基礎概念—Provider
下一篇
Day7-【入門教程】Terraform代碼的書寫及類型
系列文
Terraform 繁體中文25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言